|
mruby 4.0.0
mruby is the lightweight implementation of the Ruby language
|
ROM method tables allow C methods to be registered using static data stored in ROM (read-only memory) rather than heap-allocated RAM. This saves significant memory on embedded systems where RAM is scarce.
In a default mruby build, mrb_open() builds ~40 classes with ~700+ method entries at startup. Each method entry is heap-allocated via individual mrb_define_method_id() calls. On a constrained MCU, this consumes ~14KB of RAM for method table metadata alone.
ROM method tables eliminate this cost by placing method metadata in static const data at compile time. Only runtime mutations (e.g., reopening a class to add methods) trigger heap allocation.
Each class has a method table (mt) pointer to a linked list of mrb_mt_tbl layers:
Lookup walks the chain front-to-back, returning the first match. The method cache makes repeated lookups O(1), so the chain walk only occurs on cache misses.
Mutation uses copy-on-write (COW): if the top layer is read-only, a new mutable layer is created in front of it. The ROM data is never modified.
Each mrb_mt_tbl stores method entries as an array of mrb_mt_entry structs, each combining a function pointer, a symbol key, and flags:
Values are union mrb_mt_ptr (function pointer or proc pointer). Keys are pure mrb_sym (no flag encoding). Flags are a separate uint32_t field that stores visibility, func/proc type, and argument spec.
Entries are searched linearly, so source code order does not matter. The method cache makes repeated lookups O(1), so the linear scan only occurs on cache misses.
The const mrb_mt_entry[] arrays are truly static and shared across the process. However, the mrb_mt_tbl wrapper (which carries the next pointer for chaining) is heap-allocated per mrb_state by MRB_MT_INIT_ROM(). This allows multiple mrb_state instances in the same process to each have independent method table chains, even when linking to the same const entries.
Include <mruby/class.h> (which provides mrb_mt_entry, MRB_MT_ENTRY(), and flag constants) and define the ROM entries:
Replace mrb_define_method_id() calls with a single MRB_MT_INIT_ROM() call:
MRB_MT_INIT_ROM() allocates a per-state wrapper and pushes the ROM layer onto the class's method table chain.
Build and run the test suite. ROM tables are semantically transparent to Ruby code.
Defined in include/mruby/class.h:
| Flag | Value | Description |
|---|---|---|
| MRB_MT_FUNC | (1<<24) | C function (auto-set by macro) |
| MRB_MT_PUBLIC | 0 | Public visibility (default) |
| MRB_MT_PRIVATE | (1<<25) | Private visibility (in entry param) |
The third parameter to MRB_MT_ENTRY() is an MRB_ARGS_*() expression optionally OR'd with MRB_MT_PRIVATE. The aspec value occupies bits 0-23 and the visibility flag occupies bit 25; these ranges do not overlap, so the values are simply OR'd together. MRB_MT_FUNC is set automatically. The no-arg optimization is derived at runtime from aspec == 0 (MRB_ARGS_NONE()).
How to write entries:
Use the presym macros for keys. See doc/guides/symbol.md for the full list:
Allocates a per-state mrb_mt_tbl wrapper for the const entries and pushes it onto the class's method table chain. The wrapper is tracked in mrb->rom_mt and freed at mrb_close(). Use the MRB_MT_INIT_ROM macro to auto-compute the size. Multiple calls push additional layers, which is how extension gems add methods to core classes.
Each MRB_MT_ENTRY() bundles a function pointer with its method name and flags in a single line. Their order in the source code does not matter, but keeping related methods together improves readability.
Method aliases (two names for the same function) are expressed as separate entries sharing the same function pointer:
Methods that depend on build configuration (e.g., MRB_NO_FLOAT) can use #ifdef directly inside the ROM entries array. The sizeof in MRB_MT_INIT_ROM() automatically adjusts to the number of entries that survive preprocessing:
For conditional methods on a different class, use a separate ROM table wrapped in the #ifdef:
Extension gems use exactly the same pattern. Since gems are initialized after core, calling MRB_MT_INIT_ROM() pushes the gem's ROM layer in front of the core ROM layer:
After initialization, String's method table chain looks like:
A gem may also define ROM tables for multiple classes:
Some methods must remain as mrb_define_method_id() calls:
These methods are added after MRB_MT_INIT_ROM() and go into the mutable layer that sits in front of the ROM chain.
Ruby's open classes work transparently. When a Ruby program or C code adds a method to a class with a ROM table, the COW mechanism creates a mutable layer:
remove_method works on ROM methods using a tombstone marker. When a method in a ROM layer is removed, a special entry (MRB_MT_FUNC flag with func=NULL) is inserted into the mutable layer. The mt_get() lookup treats this marker as "not found" and stops searching the chain, effectively hiding the ROM entry. Unlike undef_method (which blocks superclass lookup), remove_method's tombstone allows the superclass method to be found.
undef_method uses a different tombstone (proc=NULL without MRB_MT_FUNC), which is returned by mt_get() so the caller raises NoMethodError without searching the superclass.
Class.dup shares the ROM chain. The duplicated class gets an empty mutable layer pointing to the same ROM layers as the original. No ROM data is copied.
ROM layers are skipped during GC mark and sweep phases. Only mutable layers are scanned for live RProc references and freed when the class is collected. ROM wrappers are freed at mrb_close() via the mrb->rom_mt tracking list.
mrb_class_mt_memsize() reports only mutable layer memory. ROM wrappers are tracked separately and not counted per-class.
To convert existing mrb_define_method_id() calls to a ROM table: